package com.aispeech.detector.utils;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.media.audiofx.AcousticEchoCanceler;
import android.util.Log;

import java.io.FileOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class VoiceRecorder {

    private final static String TAG = "VoiceRecorder";

    private ExecutorService mCachedThreadPool = Executors.newCachedThreadPool();

    private AudioManager mAudioManager;
    private AudioRecord mAudioRecorder = null;
    private int mAudioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION;
    private int mSampleRateInHz = 16000;
    private int mChannelConfig = AudioFormat.CHANNEL_IN_STEREO;
    private int mPhyChannels = 1;

    private boolean mRunning = false;
    private String mOutputFilePath = null;
    private short[] mChannelsAmplitudeArray;

    private int mAudioSessionId;
    private AcousticEchoCanceler mAec;

    public int getAudioSessionId() {
        return mAudioSessionId;
    }

    public VoiceRecorder(AudioManager audioManager) {
        mAudioManager = audioManager;
    }

    public void setOutputFile(String path) {
        mOutputFilePath = path;
    }

    public void setAudioSource(int audioSource) {
        mAudioSource = audioSource;
    }

    public int getAudioSource() {
        return mAudioSource;
    }

    public void setSampleRate(int sampleRateInHz) {
        mSampleRateInHz = sampleRateInHz;
    }

    public int getSampleRate() {
        return mSampleRateInHz;
    }

    public void setChannelConfig(int channelConfig) {
        mChannelConfig = channelConfig;
    }

    public int getChannelConfig() {
        return mChannelConfig;
    }

    public int getPhyChannels() {
        return mPhyChannels;
    }

    public void setPhyChannels(int mPhyChannels) {
        if (isRecording()) {
            Log.w(TAG, "can not setPhyChannels when recording!");
        } else {
            this.mPhyChannels = mPhyChannels;
        }
    }

    public boolean isRecording() {
        boolean bRecording = false;

        if ((mAudioRecorder != null) && (mAudioRecorder.getState() == AudioRecord.STATE_INITIALIZED)) {
            bRecording = (mAudioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING);
        }

        return bRecording;
    }

    public synchronized short[] getChannelsAmplitudeArray() {
        return mChannelsAmplitudeArray;
    }

    private short byteArrayToShort(byte[] b, int offset) {
        return (short) ((b[offset] & 0xFF) | (b[offset + 1] & 0xFF) << 8);
    }

    private void countAmplitude(byte[] buffer, int bufferLen) {
//        int channels = (mChannelConfig == AudioFormat.CHANNEL_IN_MONO) ? 1 : 2;
        int frameCount = bufferLen / mPhyChannels / 2;
        int frameSize = mPhyChannels * 2;
        int frame, offset, ch;
        short amplitue;

        //clean value
        for (int i = 0; i < mPhyChannels; ++i) {
            mChannelsAmplitudeArray[i] = 0;
        }

        for (offset = 0, frame = 0; frame < frameCount; frame++) {
            for (ch = 0; ch < mChannelsAmplitudeArray.length; ++ch) {
                amplitue = (short) Math.abs(byteArrayToShort(buffer, offset + ch * 2));
                if (amplitue > mChannelsAmplitudeArray[ch]) {
//                        amplitue = (short) (20 * Math.log10(amplitue));
                    mChannelsAmplitudeArray[ch] = amplitue;
                }
            }
            offset = frame * frameSize;
        }
    }

    private boolean createAudioAlgorithm(int sessionId) {
        boolean available = AcousticEchoCanceler.isAvailable();
        Log.d(TAG, "createAudioAlgorithm: available = " + available);
        if (available) {
            mAec = AcousticEchoCanceler.create(sessionId);
            if (mAec != null) {
                Log.d(TAG, "createAudioAlgorithm: create success");
                return true;
            }
        }
        return false;
    }

    public void setAudioAlgorithm(boolean state) {
        Log.d(TAG, "setAudioAlgorithm() called with: state = [" + state + "]");
//        NoiseSuppressor
//        AutomaticGainControl
        if (mAec == null) {
            Log.w(TAG, "setAudioAlgorithm: can not get aec");
            return;
        }
//        AudioEffect.Descriptor descriptor = aec.getDescriptor();
        boolean enabled = mAec.getEnabled();
        int ret = mAec.setEnabled(state);//0:in case of success
        Log.d(TAG, "setAudioAlgorithm: mAudioSessionId = " + mAudioSessionId);
        Log.d(TAG, "setAudioAlgorithm: get enabled = " + enabled);
        Log.d(TAG, "setAudioAlgorithm: setEnabled ret = " + ret);
        Log.d(TAG, "setAudioAlgorithm: aec enabled = " + mAec.getEnabled());
    }

    public void start() {
        if (mRunning) {
            Log.e(TAG, "Audio recorder is running, return directly!");
            return;
        }
        mChannelsAmplitudeArray = new short[mPhyChannels];

        mCachedThreadPool.execute(new Runnable() {
            public void run() {
                FileOutputStream stream = null;
                int channels = (mChannelConfig == AudioFormat.CHANNEL_IN_MONO) ? 1 : 2;
                int bufferLen = (mSampleRateInHz * channels * 2 / 10) * 4; // 400ms data
                //int bufferLen = 2048;
                byte[] buffer = new byte[bufferLen];
                int size = 0;

                try {
                    mAudioRecorder = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
                            AudioFormat.ENCODING_PCM_16BIT, mSampleRateInHz * 2 * 2);
                    Log.d(TAG, "AudioRecord(), param " + mAudioSource + "," + mSampleRateInHz + "," + mChannelConfig);

                    mAudioSessionId = mAudioRecorder.getAudioSessionId();
//                    createAudioAlgorithm(mAudioSessionId);
//                    setAudioAlgorithm(mAudioRecorder.getAudioSessionId());
                    stream = new FileOutputStream(mOutputFilePath);
                    mAudioRecorder.startRecording();
                    mRunning = true;

//                    Thread.sleep(10000);
                    Log.d(TAG, "startRecording() -- ");

                    while (mRunning) {
                        if (mAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
                            break;
                        }

                        size = mAudioRecorder.read(buffer, 0, bufferLen); //100ms
                        if (size != bufferLen) {
                            Log.e(TAG, "size " + size + " not equal bufferLen " + bufferLen);
                            //break;
                        }
                        //Log.d(TAG, "size: "+size);
                        if (size > 0) {
                            stream.write(buffer, 0, size);
                        }

                        countAmplitude(buffer, size);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    mRunning = false;
                    if (mAudioRecorder != null) {
                        if (mAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
                            mAudioRecorder.stop();
                            //Log.d(TAG, "#recorder.stop()");
                        }
                        mAudioRecorder.release();
                        mAudioRecorder = null;
                    }

                    if (stream != null) {
                        try {
                            stream.close();
                        } catch (Exception exx) {
                        }

                        stream = null;
                    }
                }
            }
        });
    }

    public void stop() {
        if (isRecording()) {
            //try {
            //    Thread.sleep(30);
            //} catch (InterruptedException e) {
            // TODO Auto-generated catch block
            //    e.printStackTrace();
            //}
            mAudioRecorder.stop();
            mRunning = false;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        if (mRunning == true) {
            stop();
        }
    }
}
